﻿using System;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Entity.Interface;

namespace RayDen.SmallPT
{
    public abstract class BaseCamera : ICamera
    {
        public Point Position { get; set; }
        public Vector Target { get; set; }
        public Vector Right { get; set; }
        public Vector Up { get; set; }
        public Vector Look { get; set; }
        public float Fov { get; set; }
        public float ZNear { get; set; }
        public float ZFar { get; set; }

        public abstract RayInfo GetRay(float x, float y);
        public abstract void LookAt(Point vector, Point vector_2, Vector vector_3);
        public void GetRay(double x, double y, out IRay cameraRay)
        {

            cameraRay = this.GetRay((float)x, (float)y);
        }

        public abstract Vector UnProject(ref Vector v);


        public void Sample(float x, float y, float u0, float u1, float u2, out IRay ray, out float pdf)
        {
            throw new NotImplementedException();
        }
    }

    public class BasicPerspectiveCamera : BaseCamera
    {

        public new float Fov
        {
            get { return fieldOfView; }
            set { fieldOfView = value; }
        }

        private float fieldOfView = 45f;
        public float clipHither = 1f, clipYon = 120f;
        Vector dir, x, y;
        private int Width;
        private int Height;

        public BasicPerspectiveCamera(Point eye, Vector dirt, Vector up, int width, int height)
        {
            this.Height = height;
            this.Width = width;
            this.Position = eye;
            this.Target = dirt;
            this.Up = up;

            this.Update();
        }

        public float FieldOfView
        {
            get { return fieldOfView; }
            set { fieldOfView = value; }
        }

        public void Update()
        {
            dir = (Target - (Vector)Position);
            dir = dir.Normalize();

            Vector up = Up;

            float k = MathLab.Radians(fieldOfView);
            x = (dir ^ up);
            x.Normalize();
            x *= Width * k / Height;

            y = (x ^ dir);
            y.Normalize();
            y *= k;
            this.CreateProjectionMatrix();
        }

        private Matrix4x4 projection;
        public void CreateProjectionMatrix()
        {
            var U = x;
            var V = y;
            var N = (Target - (Vector)Position).Normalize();
            var C = Position;
            int w = Width;
            int h = Height;
            var m1 = new Matrix4x4(w / 2.0f * ((float)Math.Tan(MathLab.Radians(fieldOfView * 0.5f))), 0, -w * 0.5f, 0,
                                   0, h / 2.0f * ((float)Math.Tan(MathLab.Radians(fieldOfView * 0.5f))), -h * 0.5f, 0,
                                   0, 0, 1, 0,
                                   0, 0, -1, 0);
            var m2 = new Matrix4x4(U.X, U.Y, U.Z, 0,
                                   V.X, V.Y, V.Z, 0,
                                   -N.X, -N.Y, -N.Z, 0,
                                   0, 0, 0, 1);
            var m3 = new Matrix4x4(1, 0, 0, -C.X,
                                   0, 1, 0, -C.Y,
                                   0, 0, 1, -C.Z,
                                   0, 0, 0, 1);
            this.projection = m1 * m2 * m3;
            projectTransform = new Transform(projection, Matrix4x4.Inverse(ref projection));

        }

        private Transform projectTransform;

        public override Vector UnProject(ref Vector v)
        {
            return projectTransform & v;
        }
        public override RayInfo GetRay(float xp, float yp)
        {

            var u = 2.0f * xp / Width - 1.0f;
            var v = 1.0f - 2.0f * yp / Height;

            Vector rdir = new Vector(x.x * u + y.x * v + dir.x, x.y * u + y.y * v + dir.y, x.z * u + y.z * v + dir.z);
            var rorig = (Position + rdir * 0.1f);
            rdir.Normalize();
            return new RayInfo(ref rorig, ref rdir);
        }

        public override void LookAt(Point vector, Point vector_2, Vector vector_3)
        {
            this.Position = vector;
            this.Target = (Vector)vector_2;
            this.Up = vector_3;
            this.Update();
        }

        public void GenerateRay(float xp, float yp, out RayInfo ray)
        {
            var u = 2.0f * xp / Width - 1.0f;
            var v = 1.0f - 2.0f * yp / Height;

            Vector rdir = x * u + y * v + dir;
            var rorig = (Position + rdir * 0.1f);
            rdir.Normalize();
            ray = new RayInfo(rorig, -rdir);
        }
    }
}